import {
  checkFilesExist,
  cleanupProject,
  getSelectedPackageManager,
  getPackageManagerCommand,
  killPorts,
  newProject,
  promisifiedTreeKill,
  readJson,
  runCLI,
  runCommand,
  runCommandUntil,
  tmpProjPath,
  uniq,
  updateFile,
  updateJson,
  getRandomPort,
} from '@nx/e2e-utils';
import { execSync } from 'child_process';
import * as http from 'http';

let originalEnvPort;

describe('Node Applications', () => {
  const pm = getSelectedPackageManager();
  let workspaceName: string;

  beforeAll(() => {
    originalEnvPort = process.env.PORT;
    workspaceName = newProject({
      packages: ['@nx/node', '@nx/express', '@nx/nest', '@nx/webpack'],
      preset: 'ts',
    });
  });

  afterAll(() => {
    process.env.PORT = originalEnvPort;
    cleanupProject();
  });

  it('should be able to generate an empty application', async () => {
    const nodeapp = uniq('nodeapp');
    const port = getRandomPort();
    process.env.PORT = `${port}`;
    runCLI(
      `generate @nx/node:app apps/${nodeapp} --port=${port} --linter=eslint --unitTestRunner=jest`
    );

    expect(() => runCLI(`lint ${nodeapp}`)).not.toThrow();
    expect(() => runCLI(`test ${nodeapp}`)).not.toThrow();

    updateFile(`apps/${nodeapp}/src/main.ts`, `console.log('Hello World!');`);
    runCLI(`build ${nodeapp}`);

    checkFilesExist(`apps/${nodeapp}/dist/main.js`);
    const result = execSync(`node apps/${nodeapp}/dist/main.js`, {
      cwd: tmpProjPath(),
    }).toString();
    expect(result).toContain('Hello World!');
    await killPorts(port);
  }, 300_000);

  it('should be able to generate an express application', async () => {
    const nodeapp = uniq('nodeapp');
    const nodelib = uniq('nodelib');
    const port = getRandomPort();
    process.env.PORT = `${port}`;
    runCLI(
      `generate @nx/express:app apps/${nodeapp} --port=${port} --linter=eslint --unitTestRunner=jest`
    );
    runCLI(
      `generate @nx/node:lib packages/${nodelib} --linter=eslint --unitTestRunner=jest`
    );

    // No tests are generated by default, add a stub one.
    updateFile(
      `apps/${nodeapp}/src/app/test.spec.ts`,
      `
          describe('test', () => {
            it('should work', () => {
              expect(true).toEqual(true);
            })
          })
        `
    );

    updateFile(`apps/${nodeapp}/src/assets/file.txt`, `Test`);
    updateFile(`apps/${nodeapp}/src/main.ts`, (content) => {
      return `import { ${nodelib} } from '@${workspaceName}/${nodelib}';\n${content}\nconsole.log(${nodelib}());`;
    });
    // pnpm does not link packages unless they are deps
    // npm, yarn, and bun will link them in the root node_modules regardless
    if (pm === 'pnpm') {
      updateJson(`apps/${nodeapp}/package.json`, (json) => {
        json.dependencies = {
          [`@${workspaceName}/${nodelib}`]: 'workspace:*',
        };
        return json;
      });
      runCommand(`cd apps/${nodeapp} && ${getPackageManagerCommand().install}`);
    }
    runCLI(`sync`);

    expect(() => runCLI(`lint ${nodeapp}`)).not.toThrow();
    expect(() => runCLI(`test ${nodeapp}`)).not.toThrow();
    expect(() => runCLI(`build ${nodeapp}`)).not.toThrow();
    expect(() => runCLI(`typecheck ${nodeapp}`)).not.toThrow();
    expect(() => runCLI(`lint ${nodelib}`)).not.toThrow();
    expect(() => runCLI(`test ${nodelib}`)).not.toThrow();
    expect(() => runCLI(`build ${nodelib}`)).not.toThrow();
    expect(() => runCLI(`typecheck ${nodelib}`)).not.toThrow();

    const p = await runCommandUntil(
      `serve ${nodeapp}`,
      (output) => output.includes(`Listening at http://localhost:${port}`),

      {
        env: {
          NX_DAEMON: 'true',
        },
      }
    );

    let result = await getData(port);
    expect(result.message).toMatch(`Welcome to ${nodeapp}!`);

    result = await getData(port, '/assets/file.txt');
    expect(result).toMatch(`Test`);

    try {
      await promisifiedTreeKill(p.pid, 'SIGKILL');
      expect(await killPorts(port)).toBeTruthy();
    } catch (err) {
      console.log(
        'Error during cleanup (may be expected, especially ECONNRESET):',
        err.message
      );
    }
  }, 300_000);

  it('should be able to generate a nest application', async () => {
    const nestapp = uniq('nodeapp');
    const port = getRandomPort();
    process.env.PORT = `${port}`;
    runCLI(
      `generate @nx/nest:app apps/${nestapp} --linter=eslint --unitTestRunner=jest`
    );

    expect(() => runCLI(`lint ${nestapp}`)).not.toThrow();
    expect(() => runCLI(`test ${nestapp}`)).not.toThrow();

    runCLI(`build ${nestapp}`);
    checkFilesExist(`apps/${nestapp}/dist/main.js`);

    const p = await runCommandUntil(
      `serve ${nestapp}`,
      (output) =>
        output.includes(
          `Application is running on: http://localhost:${port}/api`
        ),

      {
        env: {
          NX_DAEMON: 'true',
        },
      }
    );

    const result = await getData(port, '/api');
    expect(result.message).toMatch('Hello');

    try {
      await promisifiedTreeKill(p.pid, 'SIGKILL');
      expect(await killPorts(port)).toBeTruthy();
    } catch (err) {
      console.log(
        'Error during cleanup (may be expected, especially ECONNRESET):',
        err.message
      );
    }
  }, 300_000);

  it('should be able to import a lib into a nest application', async () => {
    const nestApp = uniq('nestapp');
    const nestLib = uniq('nestlib');

    const port = getRandomPort();
    process.env.PORT = `${port}`;
    runCLI(`generate @nx/nest:app apps/${nestApp} --no-interactive`);

    runCLI(`generate @nx/nest:lib packages/${nestLib} --no-interactive`);

    updateFile(`apps/${nestApp}/src/app/app.module.ts`, (content) => {
      return `import '@${workspaceName}/${nestLib}';\n${content}\n`;
    });

    if (pm === 'pnpm') {
      updateJson(`apps/${nestApp}/package.json`, (json) => {
        json.dependencies = {
          [`@${workspaceName}/${nestLib}`]: 'workspace:*',
        };
        return json;
      });
      runCommand(`${getPackageManagerCommand().install}`);
    }
    runCLI(`sync`);

    runCLI(`build ${nestApp} --verbose`);
    checkFilesExist(`apps/${nestApp}/dist/main.js`);

    const p = await runCommandUntil(
      `serve ${nestApp}`,
      (output) =>
        output.includes(
          `Application is running on: http://localhost:${port}/api`
        ),

      {
        env: {
          NX_DAEMON: 'true',
        },
      }
    );

    const result = await getData(port, '/api');
    expect(result.message).toMatch('Hello');

    try {
      await promisifiedTreeKill(p.pid, 'SIGKILL');
      expect(await killPorts(port)).toBeTruthy();
    } catch (err) {
      console.log(
        'Error during cleanup (may be expected, especially ECONNRESET):',
        err.message
      );
    }
  }, 300_000);

  // Dependencies that are not directly imported but are still required for the build to succeed
  // App -> LibA -> LibB
  // LibB is transitive, it's not imported in the app, but is required by LibA

  it('should be able to work with transitive non-dependencies', async () => {
    const nestApp = uniq('nestApp');
    const nestLibA = uniq('nestliba');
    const nestLibB = uniq('nestlibb');
    const port = getRandomPort();

    process.env.PORT = `${port}`;
    runCLI(`generate @nx/nest:app apps/${nestApp} --no-interactive`);

    runCLI(`generate @nx/nest:lib packages/${nestLibA} --no-interactive`);
    runCLI(`generate @nx/nest:lib packages/${nestLibB} --no-interactive`);

    if (pm === 'pnpm') {
      updateJson(`apps/${nestApp}/package.json`, (json) => {
        json.dependencies = {
          [`@${workspaceName}/${nestLibA}`]: 'workspace:*',
        };
        return json;
      });

      updateJson(`packages/${nestLibA}/package.json`, (json) => {
        json.dependencies = {
          [`@${workspaceName}/${nestLibB}`]: 'workspace:*',
        };
        return json;
      });
      runCommand(`${getPackageManagerCommand().install}`);
    }
    runCLI(`sync`);

    runCLI(`build ${nestApp} --verbose`);
    checkFilesExist(`apps/${nestApp}/dist/main.js`);

    const p = await runCommandUntil(
      `serve ${nestApp}`,
      (output) =>
        output.includes(
          `Application is running on: http://localhost:${port}/api`
        ),

      {
        env: {
          NX_DAEMON: 'true',
        },
      }
    );

    const result = await getData(port, '/api');
    expect(result.message).toMatch('Hello');

    try {
      await promisifiedTreeKill(p.pid, 'SIGKILL');
      expect(await killPorts(port)).toBeTruthy();
    } catch (err) {
      console.log(
        'Error during cleanup (may be expected, especially ECONNRESET):',
        err.message
      );
    }
  }, 300_000);

  it('should respect and support generating libraries with a name different than the import path', () => {
    const nodeLib = uniq('node-lib');
    const nestLib = uniq('nest-lib');

    runCLI(
      `generate @nx/node:lib packages/${nodeLib} --name=${nodeLib} --buildable`
    );
    runCLI(
      `generate @nx/nest:lib packages/${nestLib} --name=${nestLib} --buildable`
    );

    const packageJson = readJson(`packages/${nodeLib}/package.json`);
    expect(packageJson.nx.name).toBe(nodeLib);
    const nestPackageJson = readJson(`packages/${nestLib}/package.json`);
    expect(nestPackageJson.nx.name).toBe(nestLib);

    expect(runCLI(`build ${nodeLib}`)).toContain(
      `Successfully ran target build for project ${nodeLib}`
    );
    expect(runCLI(`build ${nestLib}`)).toContain(
      `Successfully ran target build for project ${nestLib}`
    );
  }, 300_000);
});

function getData(port, path = '/api'): Promise<any> {
  return new Promise((resolve) => {
    http.get(`http://localhost:${port}${path}`, (res) => {
      expect(res.statusCode).toEqual(200);
      let data = '';
      res.on('data', (chunk) => {
        data += chunk;
      });
      res.once('end', () => {
        try {
          resolve(JSON.parse(data));
        } catch (e) {
          resolve(data);
        }
      });
    });
  });
}
